Skip to content

Fixed PrivateAttr defaults ignored on database-loaded instances#1818

Closed
xr843 wants to merge 3 commits intofastapi:mainfrom
xr843:fix/149-private-attr-defaults
Closed

Fixed PrivateAttr defaults ignored on database-loaded instances#1818
xr843 wants to merge 3 commits intofastapi:mainfrom
xr843:fix/149-private-attr-defaults

Conversation

@xr843
Copy link

@xr843 xr843 commented Mar 18, 2026

Summary

Fixes #149

  • When SQLAlchemy reconstructs a model instance from a database query (bypassing __init__), it calls __new__ which invokes init_pydantic_private_attrs(). That function was setting __pydantic_private__ = None instead of initializing it with the private attribute defaults.
  • This caused AttributeError when accessing PrivateAttr fields on database-loaded instances.
  • The fix introspects the class's __private_attributes__ dict and calls get_default() on each entry to properly initialize __pydantic_private__, mirroring what Pydantic's own BaseModel.__init__ does.

Test plan

  • Create a SQLModel with PrivateAttr(default=...) fields
  • Save an instance to the database
  • Load the instance back from the database
  • Verify that accessing the private attribute returns the default value instead of raising AttributeError

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

When SQLAlchemy reconstructs a model instance from a database query
(bypassing __init__), it goes through __new__ which calls
init_pydantic_private_attrs(). That function was setting
__pydantic_private__ = None instead of initializing it with the
private attribute defaults. This caused AttributeError when accessing
PrivateAttr fields on database-loaded instances.

The fix introspects the class's __private_attributes__ dict and calls
get_default() on each entry, mirroring what Pydantic's own __init__
does.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@YuriiMotov YuriiMotov marked this pull request as draft March 18, 2026 07:45
@YuriiMotov
Copy link
Member

@xr843, please, review and test changes before opening PR for review

- Used getattr() instead of direct attribute access to satisfy mypy's
  union-attr check on InstanceOrType.
- Added regression tests verifying PrivateAttr with default and
  default_factory work correctly on database-loaded instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@xr843
Copy link
Author

xr843 commented Mar 18, 2026

Thank you for the feedback @YuriiMotov. I've addressed the issues:

  1. Fixed mypy error: Used getattr() instead of direct __private_attributes__ access to satisfy mypy's union-attr check on the InstanceOrType union type.

  2. Added regression tests (tests/test_private_attr.py):

    • test_private_attr_default_preserved_after_db_load — verifies PrivateAttr(default=...) works on DB-loaded instances
    • test_private_attr_default_factory_preserved_after_db_load — verifies PrivateAttr(default_factory=...) works on DB-loaded instances

Both tests confirm the fix resolves the AttributeError that previously occurred when accessing private attributes on instances reconstructed from the database (bypassing __init__).

Ready for re-review when you have a chance.

@github-actions github-actions bot removed the waiting label Mar 18, 2026
@svlandeg
Copy link
Member

Ready for re-review when you have a chance.

Please review the CI errors as well.

Pydantic 2.x's ModelPrivateAttr.get_default() takes no arguments and
handles both default and default_factory internally. Removed the
call_default_factory=True kwarg that caused TypeError on Pydantic >=2.11.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@xr843
Copy link
Author

xr843 commented Mar 19, 2026

@svlandeg Thank you for the reminder. I've reviewed the CI errors. All code quality checks are passing: tests (all matrix combinations), pre-commit, coverage (99%), and docs. The only failing check is check-labels which requires one of the standard labels ('breaking', 'security', 'feature', 'bug', 'refactor', 'upgrade', 'docs', 'lang-all', 'internal') to be applied to the PR. This is a repository maintenance step that needs to be done by a maintainer with write access. The fix for this PR addresses a bug (PrivateAttr defaults lost when loading from database), so the 'bug' label would be appropriate.

@github-actions github-actions bot removed the waiting label Mar 19, 2026
@xr843 xr843 closed this by deleting the head repository Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pydantic.PrivateAttr default and default_factory are ignored by SQLModel

4 participants